package com.siashan.toolkit.crypt.symmetric;

import com.siashan.toolkit.crypt.CryptException;
import com.siashan.toolkit.crypt.DecoderException;
import com.siashan.toolkit.crypt.KeyUtil;
import com.siashan.toolkit.crypt.SecureUtil;
import com.siashan.toolkit.crypt.binary.Base64;
import com.siashan.toolkit.crypt.binary.Hex;
import com.siashan.toolkit.crypt.util.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * 对称加密算法<br>
 * 在对称加密算法中，数据发信方将明文（原始数据）和加密密钥一起经过特殊加密算法处理后，使其变成复杂的加密密文发送出去。<br>
 * 收信方收到密文后，若想解读原文，则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密，才能使其恢复成可读明文。<br>
 * 在对称加密算法中，使用的密钥只有一个，发收信双方都使用这个密钥对数据进行加密和解密，这就要求解密方事先必须知道加密密钥。<br>
 *
 * @author siashan
 * @since 1.0.7
 */
public abstract class SymmetricCrypt implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final Charset UTF_8 = StandardCharsets.UTF_8;

    /**
     * Cipher负责完成加密或解密工作
     */
    private Cipher cipher;
    /**
     * 是否0填充
     */
    private boolean zeroPadding;


    public SymmetricCrypt() {
    }

    /**
     * 构建加密对象
     *
     * @param algorithm  算法 {@link SymmetricAlgorithm }
     * @param mode      加密模式
     * @param padding   填充模式
     * @return 加密对象
     */
    public SymmetricCrypt(SymmetricAlgorithm algorithm, Mode mode, Padding padding) {
        String fmtAlgorithm = SecureUtil.fmtAlgorithm(algorithm, mode, padding);
        if (fmtAlgorithm.contains(Padding.ZeroPadding.name())) {
            fmtAlgorithm = fmtAlgorithm.replace(Padding.ZeroPadding.name(), Padding.NoPadding.name());
            this.zeroPadding = true;
        }
        this.cipher = SecureUtil.createCipher(fmtAlgorithm);
    }

    public abstract SymmetricAlgorithm getAlgorithm();


    // -------------------------------------------------- Encrypt ----------------------------------------------------//

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key    秘钥,支持三种密钥长度：128、192、256位
     * @param iv    加密盐/加密向量
     * @return 加密后的bytes
     */
    private byte[] encrypt(byte[] data, SecretKey key, IvParameterSpec iv) {
        try {
            if (null == iv) {
                cipher.init(Cipher.ENCRYPT_MODE, key);
            } else {
                cipher.init(Cipher.ENCRYPT_MODE, key, iv);
            }
            return cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize(), zeroPadding));
        } catch (Exception e) {
            throw new CryptException(e);
        }
    }

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的bytes
     */
    public byte[] encrypt(byte[] data, byte[] key) {
        return encrypt(data, KeyUtil.genKey(getAlgorithm().name(), key), null);
    }

    /**
     * 加密
     *
     * @param data 数据
     * @param key  秘钥
     * @return 加密后的Hex
     */
    public String encryptHex(byte[] data, byte[] key) {
        return Hex.encodeHexString(encrypt(data, key));
    }

    /**
     * 加密
     *
     * @param data 数据
     * @param key  秘钥
     * @return 加密后的Base64
     */
    public String encryptBase64(byte[] data, byte[] key) {
        return Base64.encodeBase64String(encrypt(data, key));
    }


    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的bytes
     */
    public byte[] encrypt(byte[] data, String key) {
        return encrypt(data, key.getBytes());
    }

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的Hex
     */
    public String encryptHex(byte[] data, String key) {
        return Hex.encodeHexString(encrypt(data, key));
    }

    /**
     * 加密
     *
     * @param data 数据
     * @param key  秘钥
     * @return 加密后的Base64
     */
    public String encryptBase64(byte[] data, String key) {
        return Base64.encodeBase64String(encrypt(data, key));
    }

    /**
     * 加密
     *
     * @param data 被加密的字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的bytes
     */
    public byte[] encrypt(String data, byte[] key) {
        return encrypt(data.getBytes(UTF_8), key);
    }

    /**
     * 加密
     *
     * @param data 被加密的字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的Hex
     */
    public String encryptHex(String data, byte[] key) {
        return Hex.encodeHexString(encrypt(data, key));
    }

    /**
     * 加密
     *
     * @param data 被加密的字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的Hex
     */
    public String encryptBase64(String data, byte[] key) {
        return Base64.encodeBase64String(encrypt(data, key));
    }


    /**
     * 加密
     *
     * @param data 被加密的字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的bytes
     */
    public byte[] encrypt(String data, String key) {
        return encrypt(data.getBytes(UTF_8), key.getBytes(UTF_8));
    }

    /**
     * 加密
     *
     * @param data 被加密的字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的Hex
     */
    public String encryptHex(String data, String key) {
        return Hex.encodeHexString(encrypt(data, key));
    }

    /**
     * 加密
     *
     * @param data 被加密的字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 加密后的Hex
     */
    public String encryptBase64(String data, String key) {
        return Base64.encodeBase64String(encrypt(data, key));
    }


    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密盐/加密向量
     * @return 加密后的bytes
     */
    public byte[] encrypt(byte[] data, byte[] key, byte[] iv) {
        return encrypt(data, KeyUtil.genKey(cipher.getAlgorithm(), key), new IvParameterSpec(iv));
    }

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密盐/加密向量
     * @return 加密后的Hex
     */
    public String encryptHex(byte[] data, byte[] key, byte[] iv) {
        return Hex.encodeHexString(encrypt(data, key, iv));
    }

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密盐/加密向量
     * @return 加密后的Base64字符串
     */
    public String encryptBase64(byte[] data, byte[] key, byte[] iv) {
        return Base64.encodeBase64String(encrypt(data, key, iv));
    }

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密盐/加密向量
     * @return 加密后的bytes
     */
    public byte[] encrypt(String data, String key, String iv) {
        return encrypt(data.getBytes(UTF_8), key.getBytes(UTF_8), iv.getBytes(UTF_8));
    }

    /**
     * 加密
     *
     * @param data 被加密的明文字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密盐/加密向量
     * @return 加密后的Hex
     */
    public String encryptHex(String data, String key, String iv) {
        return Hex.encodeHexString(encrypt(data, key, iv));
    }

    /**
     * 加密
     *
     * @param data 被加密的明文字符串
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密盐/加密向量
     * @return 加密后的Base64字符串
     */
    public String encryptBase64(String data, String key, String iv) {
        return Base64.encodeBase64String(encrypt(data, key, iv));
    }


    // -------------------------------------------- Decrypt ----------------------------------------------------------//


    /**
     * 解密
     *
     * @param data 被加密的bytes
     * @param key    秘钥,支持三种密钥长度：128、192、256位
     * @param iv    加密盐/加密向量
     * @return 解密后的bytes
     */
    private byte[] decrypt(byte[] data, SecretKey key, IvParameterSpec iv) {
        final int blockSize;
        final byte[] decryptData;
        try {
            if (null == iv) {
                cipher.init(Cipher.DECRYPT_MODE, key);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, key, iv);
            }
            blockSize = cipher.getBlockSize();
            decryptData = cipher.doFinal(data);
        } catch (Exception e) {
            throw new CryptException(e);
        }
        return removePadding(decryptData, blockSize);
    }

    /**
     * 解密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @return 解密后的bytes
     */
    public byte[] decrypt(byte[] data, byte[] key) {
        return decrypt(data, KeyUtil.genKey(getAlgorithm().name(), key), null);
    }


    /**
     * 解密为字符串
     *
     * @param bytes   被解密的bytes
     * @param key 秘钥
     * @return 解密后的String
     */
    public String decryptStr(byte[] bytes, byte[] key) {
        return StringUtils.newStringUtf8(decrypt(bytes, key));
    }

    /**
     * 解密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密向量
     * @return 解密后的bytes
     */
    public byte[] decrypt(byte[] data, byte[] key, byte[] iv) {
        return decrypt(data, KeyUtil.genKey(getAlgorithm().name(), key), new IvParameterSpec(iv));
    }

    /**
     * 解密
     *
     * @param data 被加密的bytes
     * @param key  秘钥，支持三种密钥长度：128、192、256位
     * @param iv   加密向量
     * @return 解密后的bytes
     */
    public String decryptToStr(byte[] data, byte[] key, byte[] iv) {
        return StringUtils.newStringUtf8(decrypt(data, key, iv));
    }


    /**
     * 解密Hex（16进制）表示的字符串
     *
     * @param data 被解密的String，必须为16进制字符串
     * @return 解密后的bytes
     */
    public byte[] decryptHex(String data, byte[] key) throws DecoderException {
        return decrypt(Hex.decodeHex(data), key);
    }

    /**
     * 解密Hex（16进制）表示的字符串
     *
     * @param data 被解密的String，必须为16进制字符串
     * @param key  秘钥
     * @param iv   加密向量
     * @return 解密后的bytes
     */
    public byte[] decryptHex(String data, byte[] key, byte[] iv) throws DecoderException {
        return decrypt(Hex.decodeHex(data), key, iv);
    }

    /**
     * 解密Hex（16进制）表示的字符串
     *
     * @param data 被解密的String，必须为16进制字符串
     * @param key  秘钥
     * @return 解密后的字符串
     */
    public String decryptHexToStr(String data, byte[] key) throws DecoderException {
        return StringUtils.newStringUtf8(decrypt(Hex.decodeHex(data), key));
    }

    /**
     * 解密Hex（16进制）表示的字符串
     *
     * @param data 被解密的String，必须为16进制字符串
     * @param key  秘钥
     * @param iv   加密向量
     * @return 解密后的字符串
     */
    public String decryptHexToStr(String data, byte[] key, byte[] iv) throws DecoderException {
        return StringUtils.newStringUtf8(decrypt(Hex.decodeHex(data), key, iv));
    }

    /**
     * 解密Base64表示的字符串
     *
     * @param data 被解密的String，必须Base64字符串
     * @return 解密后的bytes
     */
    public byte[] decryptBase64(String data, byte[] key) {
        return decrypt(Base64.decodeBase64(data), key);
    }

    /**
     * 解密Base64表示的字符串
     *
     * @param data 被解密的String，必须Base64字符串
     * @param key  秘钥
     * @param iv   加密向量
     * @return 解密后的bytes
     */
    public byte[] decryptBase64(String data, byte[] key, byte[] iv) {
        return decrypt(Base64.decodeBase64(data), key, iv);
    }

    /**
     * 解密Base64表示的字符串
     *
     * @param data 被解密的String，必须Base64字符串
     * @return 解密后的字符串
     */
    public String decryptBase64ToStr(String data, byte[] key) {
        return StringUtils.newStringUtf8(decrypt(Base64.decodeBase64(data), key));
    }

    /**
     * 解密Base64表示的字符串
     *
     * @param data 被解密的String，必须Base64字符串
     * @param key  秘钥
     * @param iv   加密向量
     * @return 解密后的字符串
     */
    public String decryptBase64ToStr(String data, byte[] key, byte[] iv) {
        return StringUtils.newStringUtf8(decrypt(Base64.decodeBase64(data), key, iv));
    }


    // --------------------------------------------------------------------------------- Private method start

    /**
     * 数据按照blockSize的整数倍长度填充填充0
     *
     * <p>
     * 在{@link Padding#ZeroPadding} 模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据
     *
     * <p>
     * 见：https://blog.csdn.net/OrangeJack/article/details/82913804
     *
     * @param data      数据
     * @param blockSize 块大小
     * @param isZeroPadding   是否0填充
     * @return 填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据
     */
    private byte[] paddingDataWithZero(byte[] data, int blockSize, boolean isZeroPadding) {
        if (isZeroPadding) {
            final int length = data.length;
            // 按照块拆分后的数据中多余的数据
            final int remainLength = length % blockSize;
            if (remainLength > 0) {
                // 新长度为blockSize的整数倍，多余部分填充0
                return SecureUtil.resize(data, length + blockSize - remainLength);
            }
        }
        return data;
    }


    /**
     * 数据按照blockSize去除填充部分，用于解密
     *
     * <p>
     * 在{@link Padding#ZeroPadding} 模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据
     *
     * @param data      数据
     * @param blockSize 块大小
     * @return 去除填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据
     */
    private byte[] removePadding(byte[] data, int blockSize) {
        if (this.isZeroPadding()) {
            final int length = data.length;
            final int remainLength = length % blockSize;
            if (remainLength == 0) {
                // 解码后的数据正好是块大小的整数倍，说明可能存在补0的情况，去掉末尾所有的0
                int i = length - 1;
                while (i >= 0 && 0 == data[i]) {
                    i--;
                }
                return SecureUtil.resize(data, i + 1);
            }
        }
        return data;
    }
    // --------------------------------------------------------------------------------- Private method end

    // --------------------------------------- getter/setter  --------------------------------------------------------//
    public Cipher getCipher() {
        return cipher;
    }

    public void setCipher(Cipher cipher) {
        this.cipher = cipher;
    }


    public boolean isZeroPadding() {
        return zeroPadding;
    }

    public void setZeroPadding(boolean zeroPadding) {
        this.zeroPadding = zeroPadding;
    }

}
